package app

import (
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"net/http/pprof"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
	log "github.com/sirupsen/logrus"

	"github.com/dundee/gdu/v5/build"
	"github.com/dundee/gdu/v5/internal/common"
	"github.com/dundee/gdu/v5/pkg/analyze"
	"github.com/dundee/gdu/v5/pkg/device"
	gfs "github.com/dundee/gdu/v5/pkg/fs"
	"github.com/dundee/gdu/v5/pkg/timefilter"
	"github.com/dundee/gdu/v5/report"
	"github.com/dundee/gdu/v5/stdout"
	"github.com/dundee/gdu/v5/tui"
)

// UI is common interface for both terminal UI and text output
type UI interface {
	ListDevices(getter device.DevicesInfoGetter) error
	AnalyzePath(path string, parentDir gfs.Item) error
	ReadAnalysis(input io.Reader) error
	ReadFromStorage(storagePath, path string) error
	SetIgnoreDirPaths(paths []string)
	SetIgnoreDirPatterns(paths []string) error
	SetIgnoreFromFile(ignoreFile string) error
	SetIgnoreHidden(value bool)
	SetFollowSymlinks(value bool)
	SetShowAnnexedSize(value bool)
	SetAnalyzer(analyzer common.Analyzer)
	SetTimeFilter(timeFilter common.TimeFilter)
	StartUILoop() error
}

// Flags define flags accepted by Run
type Flags struct {
	Style              Style    `yaml:"style"`
	Sorting            Sorting  `yaml:"sorting"`
	CfgFile            string   `yaml:"-"`
	LogFile            string   `yaml:"log-file"`
	InputFile          string   `yaml:"input-file"`
	OutputFile         string   `yaml:"output-file"`
	IgnoreFromFile     string   `yaml:"ignore-from-file"`
	StoragePath        string   `yaml:"storage-path"`
	IgnoreDirs         []string `yaml:"ignore-dirs"`
	IgnoreDirPatterns  []string `yaml:"ignore-dir-patterns"`
	MaxCores           int      `yaml:"max-cores"`
	Top                int      `yaml:"top"`
	SequentialScanning bool     `yaml:"sequential-scanning"`
	ShowDisks          bool     `yaml:"-"`
	ShowApparentSize   bool     `yaml:"show-apparent-size"`
	ShowRelativeSize   bool     `yaml:"show-relative-size"`
	ShowAnnexedSize    bool     `yaml:"show-annexed-size"`
	ShowVersion        bool     `yaml:"-"`
	ShowItemCount      bool     `yaml:"show-item-count"`
	ShowMTime          bool     `yaml:"show-mtime"`
	NoColor            bool     `yaml:"no-color"`
	Mouse              bool     `yaml:"mouse"`
	NonInteractive     bool     `yaml:"non-interactive"`
	NoProgress         bool     `yaml:"no-progress"`
	NoUnicode          bool     `yaml:"no-unicode"`
	NoCross            bool     `yaml:"no-cross"`
	NoHidden           bool     `yaml:"no-hidden"`
	NoDelete           bool     `yaml:"no-delete"`
	NoSpawnShell       bool     `yaml:"no-spawn-shell"`
	FollowSymlinks     bool     `yaml:"follow-symlinks"`
	Profiling          bool     `yaml:"profiling"`
	ConstGC            bool     `yaml:"const-gc"`
	UseStorage         bool     `yaml:"use-storage"`
	ReadFromStorage    bool     `yaml:"read-from-storage"`
	Summarize          bool     `yaml:"summarize"`
	UseSIPrefix        bool     `yaml:"use-si-prefix"`
	NoPrefix           bool     `yaml:"no-prefix"`
	WriteConfig        bool     `yaml:"-"`
	ReverseSort        bool     `yaml:"reverse-sort"`
	ChangeCwd          bool     `yaml:"change-cwd"`
	DeleteInBackground bool     `yaml:"delete-in-background"`
	DeleteInParallel   bool     `yaml:"delete-in-parallel"`
	Since              string   `yaml:"since"`
	Until              string   `yaml:"until"`
	MaxAge             string   `yaml:"max-age"`
	MinAge             string   `yaml:"min-age"`
}

// ShouldRunInNonInteractiveMode checks if the application should run in non-interactive mode
// based on the flags set.
func (f *Flags) ShouldRunInNonInteractiveMode(istty bool) bool {
	return !istty ||
		f.ShowVersion ||
		f.NonInteractive ||
		f.OutputFile != "" ||
		f.NoPrefix ||
		f.NoProgress ||
		f.Summarize ||
		f.Top > 0
}

// Style define style config
type Style struct {
	Footer        FooterColorStyle    `yaml:"footer"`
	SelectedRow   ColorStyle          `yaml:"selected-row"`
	ResultRow     ResultRowColorStyle `yaml:"result-row"`
	Header        HeaderColorStyle    `yaml:"header"`
	ProgressModal ProgressModalOpts   `yaml:"progress-modal"`
	UseOldSizeBar bool                `yaml:"use-old-size-bar"`
}

// ProgressModalOpts defines options for progress modal
type ProgressModalOpts struct {
	CurrentItemNameMaxLen int `yaml:"current-item-path-max-len"`
}

// ColorStyle defines styling of some item
type ColorStyle struct {
	TextColor       string `yaml:"text-color"`
	BackgroundColor string `yaml:"background-color"`
}

// FooterColorStyle defines styling of footer
type FooterColorStyle struct {
	TextColor       string `yaml:"text-color"`
	BackgroundColor string `yaml:"background-color"`
	NumberColor     string `yaml:"number-color"`
}

// HeaderColorStyle defines styling of header
type HeaderColorStyle struct {
	TextColor       string `yaml:"text-color"`
	BackgroundColor string `yaml:"background-color"`
	Hidden          bool   `yaml:"hidden"`
}

// ResultRowColorStyle defines styling of result row
type ResultRowColorStyle struct {
	NumberColor    string `yaml:"number-color"`
	DirectoryColor string `yaml:"directory-color"`
}

// Sorting defines default sorting of items
type Sorting struct {
	By    string `yaml:"by"`
	Order string `yaml:"order"`
}

// App defines the main application
type App struct {
	Writer      io.Writer
	TermApp     common.TermApplication
	Screen      tcell.Screen
	Getter      device.DevicesInfoGetter
	Flags       *Flags
	PathChecker func(string) (fs.FileInfo, error)
	Args        []string
	Istty       bool
}

func init() {
	http.DefaultServeMux = http.NewServeMux()
}

// Run starts gdu main logic
func (a *App) Run() error {
	var ui UI

	if a.Flags.ShowVersion {
		fmt.Fprintln(a.Writer, "Version:\t", build.Version)
		fmt.Fprintln(a.Writer, "Built time:\t", build.Time)
		fmt.Fprintln(a.Writer, "Built user:\t", build.User)
		return nil
	}

	log.Printf("Runtime flags: %+v", *a.Flags)

	if a.Flags.NoPrefix && a.Flags.UseSIPrefix {
		return fmt.Errorf("--no-prefix and --si cannot be used at once")
	}

	path := a.getPath()
	path, err := filepath.Abs(path)
	if err != nil {
		return err
	}

	ui, err = a.createUI()
	if err != nil {
		return err
	}

	if a.Flags.UseStorage {
		ui.SetAnalyzer(analyze.CreateStoredAnalyzer(a.Flags.StoragePath))
	}
	if a.Flags.SequentialScanning {
		ui.SetAnalyzer(analyze.CreateSeqAnalyzer())
	}
	if a.Flags.FollowSymlinks {
		ui.SetFollowSymlinks(true)
	}
	if a.Flags.ShowAnnexedSize {
		ui.SetShowAnnexedSize(true)
	}

	// Set up time filter if any time flags are provided
	if a.Flags.Since != "" || a.Flags.Until != "" || a.Flags.MaxAge != "" || a.Flags.MinAge != "" {
		if err := a.setTimeFilters(ui); err != nil {
			return err
		}
	}
	if err := a.setNoCross(path); err != nil {
		return err
	}

	ui.SetIgnoreDirPaths(a.Flags.IgnoreDirs)

	if len(a.Flags.IgnoreDirPatterns) > 0 {
		if err := ui.SetIgnoreDirPatterns(a.Flags.IgnoreDirPatterns); err != nil {
			return err
		}
	}

	if a.Flags.IgnoreFromFile != "" {
		if err := ui.SetIgnoreFromFile(a.Flags.IgnoreFromFile); err != nil {
			return err
		}
	}

	if a.Flags.NoHidden {
		ui.SetIgnoreHidden(true)
	}

	a.setMaxProcs()

	if err := a.runAction(ui, path); err != nil {
		return err
	}

	return ui.StartUILoop()
}

func (a *App) getPath() string {
	if len(a.Args) == 1 {
		return a.Args[0]
	}
	return "."
}

func (a *App) setMaxProcs() {
	if a.Flags.MaxCores < 1 || a.Flags.MaxCores > runtime.NumCPU() {
		return
	}

	runtime.GOMAXPROCS(a.Flags.MaxCores)

	// runtime.GOMAXPROCS(n) with n < 1 doesn't change current setting so we use it to check current value
	log.Printf("Max cores set to %d", runtime.GOMAXPROCS(0))
}

func (a *App) setTimeFilters(ui UI) error {
	loc := time.Local
	now := time.Now()

	timeFilter, err := timefilter.NewTimeFilter(
		a.Flags.Since,
		a.Flags.Until,
		a.Flags.MaxAge,
		a.Flags.MinAge,
		now,
		loc,
	)
	if err != nil {
		return fmt.Errorf("invalid time filter: %w", err)
	}

	if !timeFilter.IsEmpty() {
		timeFilterFunc := func(mtime time.Time) bool {
			return timeFilter.IncludeByTimeFilter(mtime, loc)
		}
		ui.SetTimeFilter(timeFilterFunc)

		// If this is a TUI, also set the filter info for display
		if tuiUI, ok := ui.(*tui.UI); ok {
			tuiUI.SetTimeFilterWithInfo(timeFilter, loc)
		}
	}
	return nil
}

func (a *App) createUI() (UI, error) {
	var ui UI

	switch {
	case a.Flags.OutputFile != "":
		var output io.Writer
		var err error
		if a.Flags.OutputFile == "-" {
			output = os.Stdout
		} else {
			output, err = os.OpenFile(a.Flags.OutputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
			if err != nil {
				return nil, fmt.Errorf("opening output file: %w", err)
			}
		}
		ui = report.CreateExportUI(
			a.Writer,
			output,
			!a.Flags.NoColor && a.Istty,
			!a.Flags.NoProgress && a.Istty,
			a.Flags.ConstGC,
			a.Flags.UseSIPrefix,
		)
	case a.Flags.ShouldRunInNonInteractiveMode(a.Istty):
		stdoutUI := stdout.CreateStdoutUI(
			a.Writer,
			!a.Flags.NoColor && a.Istty,
			!a.Flags.NoProgress && a.Istty,
			a.Flags.ShowApparentSize,
			a.Flags.ShowRelativeSize,
			a.Flags.Summarize,
			a.Flags.ConstGC,
			a.Flags.UseSIPrefix,
			a.Flags.NoPrefix,
			a.Flags.Top,
			a.Flags.ReverseSort,
		)
		if a.Flags.NoUnicode {
			stdoutUI.UseOldProgressRunes()
		}
		ui = stdoutUI
	default:
		opts := a.getOptions()

		ui = tui.CreateUI(
			a.TermApp,
			a.Screen,
			os.Stdout,
			!a.Flags.NoColor,
			a.Flags.ShowApparentSize,
			a.Flags.ShowRelativeSize,
			a.Flags.ConstGC,
			a.Flags.UseSIPrefix,
			opts...,
		)

		if !a.Flags.NoColor {
			tview.Styles.TitleColor = tcell.NewRGBColor(27, 161, 227)
		} else {
			tview.Styles.ContrastBackgroundColor = tcell.NewRGBColor(150, 150, 150)
		}
		tview.Styles.BorderColor = tcell.ColorDefault
	}

	return ui, nil
}

func (a *App) getOptions() []tui.Option {
	var opts []tui.Option

	if a.Flags.Style.SelectedRow.TextColor != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetSelectedTextColor(tcell.GetColor(a.Flags.Style.SelectedRow.TextColor))
		})
	}
	if a.Flags.Style.SelectedRow.BackgroundColor != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetSelectedBackgroundColor(tcell.GetColor(a.Flags.Style.SelectedRow.BackgroundColor))
		})
	}
	if a.Flags.Style.Footer.TextColor != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetFooterTextColor(a.Flags.Style.Footer.TextColor)
		})
	}
	if a.Flags.Style.Footer.BackgroundColor != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetFooterBackgroundColor(a.Flags.Style.Footer.BackgroundColor)
		})
	}
	if a.Flags.Style.Footer.NumberColor != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetFooterNumberColor(a.Flags.Style.Footer.NumberColor)
		})
	}
	if a.Flags.Style.Header.TextColor != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetHeaderTextColor(a.Flags.Style.Header.TextColor)
		})
	}
	if a.Flags.Style.Header.BackgroundColor != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetHeaderBackgroundColor(a.Flags.Style.Header.BackgroundColor)
		})
	}
	if a.Flags.Style.Header.Hidden {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetHeaderHidden()
		})
	}
	if a.Flags.Style.ResultRow.NumberColor != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetResultRowNumberColor(a.Flags.Style.ResultRow.NumberColor)
		})
	}
	if a.Flags.Style.ResultRow.DirectoryColor != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetResultRowDirectoryColor(a.Flags.Style.ResultRow.DirectoryColor)
		})
	}
	if a.Flags.Style.ProgressModal.CurrentItemNameMaxLen > 0 {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetCurrentItemNameMaxLen(a.Flags.Style.ProgressModal.CurrentItemNameMaxLen)
		})
	}
	if a.Flags.Style.UseOldSizeBar || a.Flags.NoUnicode {
		opts = append(opts, func(ui *tui.UI) {
			ui.UseOldSizeBar()
		})
	}
	if a.Flags.Sorting.Order != "" || a.Flags.Sorting.By != "" {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetDefaultSorting(a.Flags.Sorting.By, a.Flags.Sorting.Order)
		})
	}
	if a.Flags.ChangeCwd {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetChangeCwdFn(os.Chdir)
		})
	}
	if a.Flags.ShowItemCount {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetShowItemCount()
		})
	}
	if a.Flags.ShowMTime {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetShowMTime()
		})
	}
	if a.Flags.NoDelete {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetNoDelete()
		})
	}
	if a.Flags.NoSpawnShell {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetNoSpawnShell()
		})
	}
	if a.Flags.DeleteInBackground {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetDeleteInBackground()
		})
	}
	if a.Flags.DeleteInParallel {
		opts = append(opts, func(ui *tui.UI) {
			ui.SetDeleteInParallel()
		})
	}
	return opts
}

func (a *App) setNoCross(path string) error {
	if a.Flags.NoCross {
		mounts, err := a.Getter.GetMounts()
		if err != nil {
			return fmt.Errorf("loading mount points: %w", err)
		}
		paths := device.GetNestedMountpointsPaths(path, mounts)
		log.Printf("Ignoring mount points: %s", strings.Join(paths, ", "))
		a.Flags.IgnoreDirs = append(a.Flags.IgnoreDirs, paths...)
	}
	return nil
}

func (a *App) runAction(ui UI, path string) error {
	if a.Flags.Profiling {
		go func() {
			http.HandleFunc("/debug/pprof/", pprof.Index)
			http.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
			http.HandleFunc("/debug/pprof/profile", pprof.Profile)
			http.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
			http.HandleFunc("/debug/pprof/trace", pprof.Trace)
			log.Println(http.ListenAndServe("localhost:6060", nil))
		}()
	}

	switch {
	case a.Flags.ShowDisks:
		if err := ui.ListDevices(a.Getter); err != nil {
			return fmt.Errorf("loading mount points: %w", err)
		}
	case a.Flags.InputFile != "":
		var input io.Reader
		var err error
		if a.Flags.InputFile == "-" {
			input = os.Stdin
		} else {
			input, err = os.OpenFile(a.Flags.InputFile, os.O_RDONLY, 0o600)
			if err != nil {
				return fmt.Errorf("opening input file: %w", err)
			}
		}

		if err := ui.ReadAnalysis(input); err != nil {
			return fmt.Errorf("reading analysis: %w", err)
		}
	case a.Flags.ReadFromStorage:
		ui.SetAnalyzer(analyze.CreateStoredAnalyzer(a.Flags.StoragePath))
		if err := ui.ReadFromStorage(a.Flags.StoragePath, path); err != nil {
			return fmt.Errorf("reading from storage (%s): %w", a.Flags.StoragePath, err)
		}
	default:
		if build.RootPathPrefix != "" {
			path = build.RootPathPrefix + path
		}

		_, err := a.PathChecker(path)
		if err != nil {
			return err
		}

		log.Printf("Analyzing path: %s", path)
		if err := ui.AnalyzePath(path, nil); err != nil {
			return fmt.Errorf("scanning dir: %w", err)
		}
	}
	return nil
}
